/*
 * Created on Feb 16, 2005
 * Created by dfhuynh
 */
package edu.mit.simile.longwell.query.bucket;

import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.ResourceBundle;
import java.util.Set;

import edu.mit.simile.longwell.Cache;
import edu.mit.simile.longwell.CascadedCache;
import edu.mit.simile.longwell.Profile;
import edu.mit.simile.longwell.query.project.IDateTimeProjection;
import edu.mit.simile.longwell.query.project.IProjection;

/**
 * @author dfhuynh
 */
public class DateTimeBucketer extends BucketerBase {
	final static protected int	s_mode_byHour = 0;
	final static protected int	s_mode_byDay = 1;
	final static protected int	s_mode_byWeek = 2;
	final static protected int	s_mode_byMonth = 3;
	final static protected int	s_mode_byYear = 4;
	final static protected int	s_mode_byDecade = 5;
	final static protected int	s_mode_byCentury = 6;
	final static protected int	s_mode_relative = 7;
	
	final static protected String[] s_modePrefixes = new String[] {
		"byHour:",
		"byDay:",
		"byWeek:",
		"byMonth:",
		"byYear:",
		"byDecade:",
		"byCentury:",
		"byRelative:"
	};
	
	final static protected int	s_relativeMode_today = 0;
	final static protected int	s_relativeMode_thisWeek = 1;
	final static protected int	s_relativeMode_thisMonth = 2;
	final static protected int	s_relativeMode_thisYear = 3;
	final static protected int	s_relativeMode_withinAWeek = 4;
	final static protected int	s_relativeMode_withinAMonth = 5;
	final static protected int	s_relativeMode_within3Months = 6;
	final static protected int	s_relativeMode_withinHalfAYear = 7;
	final static protected int	s_relativeMode_withinAYear = 8;
	
	final static protected int	s_relativeMode_yesterday = 9;
	final static protected int	s_relativeMode_lastWeek = 10;
	final static protected int	s_relativeMode_lastMonth = 11;
	final static protected int	s_relativeMode_lastYear = 12;
	final static protected int	s_relativeMode_before1Week = 13;
	final static protected int	s_relativeMode_before1Month = 14;
	final static protected int	s_relativeMode_before3Months = 15;
	final static protected int	s_relativeMode_beforeHalfAYear = 16;
	final static protected int	s_relativeMode_beforeAYear = 17;

	final static protected int	s_relativeMode_tomorrow = 18;
	final static protected int	s_relativeMode_nextWeek = 19;
	final static protected int	s_relativeMode_nextMonth = 20;
	final static protected int	s_relativeMode_nextYear = 21;
	
	final static protected int	s_relativeMode_max = 22;
	
	final static protected String[] s_relativeModePrefixes = new String[] {
		"Today",
		"ThisWeek",
		"ThisMonth",
		"ThisYear",
		"WithinAWeek",
		"WithinAMonth",
		"Within3Months",
		"WithinHalfAYear",
		"WithinAYear",
		"Yesterday",
		"LastWeek",
		"LastMonth",
		"LastYear",
		"BeforeAWeek",
		"BeforeAMonth",
		"BeforeHalfAYear",
		"BeforeAYear",
		"Tomorrow",
		"NextWeek",
		"NextMonth",
		"NextYear"
	};
	
	final protected CascadedCache	m_dateRangeParamToProjectionToResult = 
		new CascadedCache(
			DateTimeBucketer.class,
			"date-range-param", 10,
			"projection", 10
		);
	
	final protected Date[] 	m_relativeStartDates = new Date[s_relativeMode_max];
	final protected Date[] 	m_relativeEndDates = new Date[s_relativeMode_max];
	final protected Cache[] m_relativeCaches = new Cache[s_relativeMode_max];

	final protected Cache m_fixedNarrowingSuggestionCache = 
		new Cache(10, DateTimeBucketer.class, "fixed-narrowing-suggestion");
	
	/**
	 * @param profile
	 */
	public DateTimeBucketer(Profile profile) {
		super(profile);
		
		for (int i = 0; i < m_relativeCaches.length; i++) {
			m_relativeCaches[i] = new Cache(10, DateTimeBucketer.class, "relative-cache");
		}
	}
	
	/* (non-Javadoc)
	 * @see edu.mit.simile.longwell.query.bucket.BucketerBase#matchesOneValue(java.lang.String)
	 */
	public boolean matchesOneValue(String parameter) {
		return false;
	}

	/* (non-Javadoc)
	 * @see edu.mit.simile.longwell.query.bucket.BucketerBase#getBucket(edu.mit.simile.longwell.query.project.IProjection, java.lang.String)
	 */
	public Set getBucket(IProjection projection, String parameter) {
		Set oldObjects = (Set) m_projectionToParameterToBucket.get(projection, parameter);
		Set newObjects = internalGetBucket(projection, parameter);
		
		if (newObjects.equals(oldObjects)) {
			return oldObjects;
		} else {
			m_projectionToParameterToBucket.put(projection, parameter, newObjects);
			return newObjects;
		}
	}

	/* (non-Javadoc)
	 * @see edu.mit.simile.longwell.query.bucket.BucketerBase#internalGetBucket(edu.mit.simile.longwell.query.project.IProjection, java.lang.String)
	 */
	protected Set internalGetBucket(IProjection projection, String parameter) {
		String[] params = splitParameter(parameter);
		Set objects = new HashSet();	
		
		for (int i = 0; i < params.length; i++) {
			internalGetOneBucket(projection, params[i], objects);
		}
		return objects;
	}

	protected void internalGetOneBucket(IProjection projection, String parameter, Set objects) {
		IDateTimeProjection dtProjection = (IDateTimeProjection) projection;
		Set newObjects = null;
		
		int mode = parameterToMode(parameter);
		if (mode == s_modePrefixes.length) {
			return;
		}

		String arguments = parameter.substring(s_modePrefixes[mode].length());
		if (mode != s_mode_relative) {
			newObjects = (Set) m_dateRangeParamToProjectionToResult.get(arguments, projection);
			if (newObjects == null) {
				String[] dateStrings = arguments.split(",");
				
				Date start = new Date(Long.parseLong(dateStrings[0]));
				Date end = new Date(Long.parseLong(dateStrings[1]));
				
				newObjects = dtProjection.getObjects(start, end);
				
				m_dateRangeParamToProjectionToResult.put(arguments, projection, newObjects);
			}
		} else {
			int relativeMode = parameterToRelativeMode(arguments);
			if (relativeMode == s_relativeModePrefixes.length) {
				return;
			}
			
			Date[] dates = relativeModeToDates(relativeMode);
			if (datesEqual(dates[0], m_relativeStartDates[relativeMode]) &&
				datesEqual(dates[1], m_relativeEndDates[relativeMode])) {
				
				newObjects = (Set) m_relativeCaches[relativeMode].get(projection);
			} else {
				m_relativeStartDates[relativeMode] = dates[0];
				m_relativeEndDates[relativeMode] = dates[1];
				m_relativeCaches[relativeMode].clear();
			}
			
			if (newObjects == null) {
				newObjects = dtProjection.getObjects(dates[0], dates[1]);
				m_relativeCaches[relativeMode].put(projection, newObjects);
			}
		}
		
		if (newObjects != null) {
			objects.addAll(newObjects);
		}
	}
	
	final static protected long	s_day = 24 * 60 * 60 * 1000;
	final static protected long	s_week = s_day * 7;
	final static protected long s_month = s_day * 30;
	final static protected long s_year = s_day * 365;
	final static protected long s_decade = s_year * 10;
	final static protected long s_century = s_year * 100;
	
	
	/* (non-Javadoc)
	 * @see edu.mit.simile.longwell.query.bucket.BucketerBase#suggestNarrowingBuckets(edu.mit.simile.longwell.query.project.IProjection, float)
	 */
	public List suggestNarrowingBuckets(IProjection projection, float desirable) {
		return internalSuggestNarrowingBuckets(projection, desirable);
	}
	
	/* (non-Javadoc)
	 * @see edu.mit.simile.longwell.query.bucket.BucketerBase#internalSuggestNarrowingBuckets(edu.mit.simile.longwell.query.project.IProjection, float)
	 */
	protected List internalSuggestNarrowingBuckets(
			IProjection projection, float desirable) {
		IDateTimeProjection dtProjection = (IDateTimeProjection) projection;
		List bucketThemes = new ArrayList();
		
		List fixedNarrowingSuggestions = 
			(List) m_fixedNarrowingSuggestionCache.get(projection);
		
		if (fixedNarrowingSuggestions == null || desirable == 1) {
			fixedNarrowingSuggestions =
				internalSuggestBuckets(dtProjection, desirable, null, "");
			m_fixedNarrowingSuggestionCache.put(
				projection, fixedNarrowingSuggestions);
		}		

		bucketThemes.addAll(fixedNarrowingSuggestions);
		
		suggestRelative(dtProjection, desirable, bucketThemes, null, "");
		
		return bucketThemes;
	}
	
	protected List internalSuggestBuckets(
		IDateTimeProjection projection, 
		float desirable, 
		String[] excepts, String concatWith) {
		
		List bucketThemes = new ArrayList();
		
		Date earliest = projection.getEarliest();
		Date latest = projection.getLatest();
		long diff = latest.getTime() - earliest.getTime();
		
		if (diff > s_century * 5) {
			suggestNarrowingByCentury(projection, desirable, bucketThemes, excepts, concatWith);
		} else if (diff > s_decade * 5) {
			suggestNarrowingByDecade(projection, desirable, bucketThemes, excepts, concatWith);
		} else if (diff > s_year * 5) {
			suggestNarrowingByYear(projection, desirable, bucketThemes, excepts, concatWith);
		} else if (diff > s_month * 3) {
			suggestNarrowingByMonth(projection, desirable, bucketThemes, excepts, concatWith);
		} else if (diff > s_week * 3) {
			suggestNarrowingByWeek(projection, desirable, bucketThemes, excepts, concatWith);
		} else if (diff > s_day) {
			suggestNarrowingByDay(projection, desirable, bucketThemes, excepts, concatWith);
		} else {
			suggestNarrowingByHour(projection, desirable, bucketThemes, excepts, concatWith);
		}
		
		return bucketThemes;
	}
	
	protected void suggestNarrowingByCentury(
		IDateTimeProjection projection, float desirable, List bucketThemes, 
		String[] excepts, String concatWith) {
		
		int totalCount = projection.countObjects();
		
		DateFormat df = new SimpleDateFormat("yyyy");
		
		Calendar earliest = Calendar.getInstance();
		Calendar latest = Calendar.getInstance();
		
		earliest.setTime(projection.getEarliest());
		latest.setTime(projection.getLatest());
		
		int year = earliest.get(Calendar.YEAR);
		
		Calendar earliestYear = Calendar.getInstance();
		earliestYear.clear();
		earliestYear.set(Calendar.YEAR, year);
		earliestYear.add(Calendar.YEAR, -(year % 100));
		
		List buckets = new ArrayList();
		
		outer:while (earliest.before(latest)) {
			Date start = earliestYear.getTime();
			earliestYear.add(Calendar.YEAR, 100);
			Date end = earliestYear.getTime();
			
			int count = projection.countObjects(start, end);
			if (count > 0 && count < totalCount) {
				String parameter = s_modePrefixes[s_mode_byCentury] + 
					start.getTime() + "," + end.getTime();
				
				if (excepts != null) {
					for (int n = 0; n < excepts.length; n++) {
						if (parameter.equals(excepts[n])) {
							continue outer;
						}
					}
				}
				
				buckets.add(
					new DateBucket(
						this.getClass().getName(), 
						concatWith + encodeParameter(parameter),
						df.format(start), 
						count,
						start));
			}
		}
		
		if (buckets.size() > 0) {
			bucketThemes.add(new BucketTheme(
				getResources(projection.getLocale()).getString("ByCentury"),
				"distinct-values",
				buckets
			));
		}
	}
	
	protected void suggestNarrowingByDecade(
		IDateTimeProjection projection, float desirable, List bucketThemes,
		String[] excepts, String concatWith) {
		
		int totalCount = projection.countObjects();
		
		DateFormat df = new SimpleDateFormat("yyyy");
		
		Calendar earliest = Calendar.getInstance();
		Calendar latest = Calendar.getInstance();
		
		earliest.setTime(projection.getEarliest());
		latest.setTime(projection.getLatest());
		
		int year = earliest.get(Calendar.YEAR);
		
		Calendar earliestYear = Calendar.getInstance();
		earliestYear.clear();
		earliestYear.set(Calendar.YEAR, year);
		earliestYear.add(Calendar.YEAR, -(year % 10));
		
		List buckets = new ArrayList();
		
		outer:while (earliestYear.before(latest)) {
			Date start = earliestYear.getTime();
			earliestYear.add(Calendar.YEAR, 10);
			Date end = earliestYear.getTime();
			
			int count = projection.countObjects(start, end);
			if (count > 0 && count < totalCount) {
				String parameter = s_modePrefixes[s_mode_byDecade] + 
					start.getTime() + "," + end.getTime();
				
				if (excepts != null) {
					for (int n = 0; n < excepts.length; n++) {
						if (parameter.equals(excepts[n])) {
							continue outer;
						}
					}
				}
				
				buckets.add(
					new DateBucket(
						this.getClass().getName(), 
						concatWith + encodeParameter(parameter),
						df.format(start), 
						count,
						start));
			}
		}
		
		if (buckets.size() > 0) {
			bucketThemes.add(new BucketTheme(
				getResources(projection.getLocale()).getString("ByDecade"),
				"distinct-values",
				buckets
			));
		}
	}

	protected void suggestNarrowingByYear(
		IDateTimeProjection projection, float desirable, List bucketThemes,
		String[] excepts, String concatWith) {
		
		int totalCount = projection.countObjects();
		
		DateFormat df = new SimpleDateFormat("yyyy");
		
		Calendar earliest = Calendar.getInstance();
		Calendar latest = Calendar.getInstance();
		
		earliest.setTime(projection.getEarliest());
		latest.setTime(projection.getLatest());
		
		Calendar earliestYear = Calendar.getInstance();
		earliestYear.clear();
		earliestYear.set(Calendar.YEAR, earliest.get(Calendar.YEAR));
		
		List buckets = new ArrayList();
		
		outer:while (earliestYear.before(latest)) {
			Date start = earliestYear.getTime();
			earliestYear.add(Calendar.YEAR, 1);
			Date end = earliestYear.getTime();
			
			int count = projection.countObjects(start, end);
			if (count > 0 && count < totalCount) {
				String parameter = s_modePrefixes[s_mode_byYear] + 
					start.getTime() + "," + end.getTime();
				
				if (excepts != null) {
					for (int n = 0; n < excepts.length; n++) {
						if (parameter.equals(excepts[n])) {
							continue outer;
						}
					}
				}
				
				buckets.add(
					new DateBucket(
						this.getClass().getName(), 
						concatWith + encodeParameter(parameter),
						df.format(start), 
						count,
						start));
			}
		}
		
		if (buckets.size() > 0) {
			bucketThemes.add(new BucketTheme(
				getResources(projection.getLocale()).getString("ByYear"),
				"distinct-values",
				buckets
			));
		}
	}
	
	protected void suggestNarrowingByMonth(
		IDateTimeProjection projection, float desirable, List bucketThemes,
		String[] excepts, String concatWith) {
		DateFormat df = new SimpleDateFormat("MMMM yyyy");
		
		int totalCount = projection.countObjects();
		
		Calendar earliest = Calendar.getInstance();
		Calendar latest = Calendar.getInstance();
		
		earliest.setTime(projection.getEarliest());
		latest.setTime(projection.getLatest());
		
		Calendar earliestMonth = Calendar.getInstance();
		earliestMonth.clear();
		earliestMonth.set(Calendar.YEAR, earliest.get(Calendar.YEAR));
		earliestMonth.set(Calendar.MONTH, earliest.get(Calendar.MONTH));
		
		List buckets = new ArrayList();
		
		outer:while (earliestMonth.before(latest)) {
			Date start = (Date) earliestMonth.getTime().clone();
			earliestMonth.add(Calendar.MONTH, 1);
			Date end = earliestMonth.getTime();
			
			int count = projection.countObjects(start, end);
			if (count > 0 && count < totalCount) {
				String parameter = s_modePrefixes[s_mode_byMonth] + 
					start.getTime() + "," + end.getTime();
				
				if (excepts != null) {
					for (int n = 0; n < excepts.length; n++) {
						if (parameter.equals(excepts[n])) {
							continue outer;
						}
					}
				}
				
				buckets.add(
					new DateBucket(
						this.getClass().getName(), 
						concatWith + encodeParameter(parameter),
						df.format(start), 
						count,
						start));
			}
		}
		
		if (buckets.size() > 0) {
			bucketThemes.add(new BucketTheme(
				getResources(projection.getLocale()).getString("ByMonth"),
				"distinct-values",
				buckets
			));
		}
	}
	
	protected void suggestNarrowingByWeek(
		IDateTimeProjection projection, float desirable, List bucketThemes,
		String[] excepts, String concatWith) {
		
		String weekOfFormat = getResources(projection.getLocale()).getString("WeekOfFormat");
		
		int totalCount = projection.countObjects();
		
		Calendar earliest = Calendar.getInstance();
		Calendar latest = Calendar.getInstance();
		
		earliest.setTime(projection.getEarliest());
		latest.setTime(projection.getLatest());
		
		Calendar earliestWeek = Calendar.getInstance();
		clearThingsInDay(earliestWeek);
		earliestWeek.clear(Calendar.DAY_OF_WEEK);
		
		List buckets = new ArrayList();
		
		outer:while (earliestWeek.before(latest)) {
			Date start = (Date) earliestWeek.getTime().clone();
			earliestWeek.add(Calendar.MONTH, 1);
			Date end = earliestWeek.getTime();
			
			int count = projection.countObjects(start, end);
			if (count > 0 && count < totalCount) {
				String parameter = s_modePrefixes[s_mode_byWeek] + 
					start.getTime() + "," + end.getTime();
				
				if (excepts != null) {
					for (int n = 0; n < excepts.length; n++) {
						if (parameter.equals(excepts[n])) {
							continue outer;
						}
					}
				}
				
				buckets.add(
					new DateBucket(
						this.getClass().getName(), 
						concatWith + encodeParameter(parameter),
						MessageFormat.format(
								weekOfFormat,
								new Object[] {start}), 
						count,
						start));
			}
		}
		
		if (buckets.size() > 0) {
			bucketThemes.add(new BucketTheme(
				getResources(projection.getLocale()).getString("ByWeek"),
				"distinct-values",
				buckets
			));
		}
	}

	protected void suggestNarrowingByDay(
		IDateTimeProjection projection, float desirable, List bucketThemes,
		String[] excepts, String concatWith) {
		
		String weekOfFormat = getResources(projection.getLocale()).getString("DayOfFormat");
		
		int totalCount = projection.countObjects();
		
		Calendar earliest = Calendar.getInstance();
		Calendar latest = Calendar.getInstance();
		
		earliest.setTime(projection.getEarliest());
		latest.setTime(projection.getLatest());
		
		Calendar earliestDay = Calendar.getInstance();
		clearThingsInDay(earliestDay);
		
		List buckets = new ArrayList();
		
		outer:while (earliestDay.before(latest)) {
			Date start = (Date) earliestDay.getTime().clone();
			earliestDay.add(Calendar.DATE, 1);
			Date end = earliestDay.getTime();
			
			int count = projection.countObjects(start, end);
			if (count > 0 && count < totalCount) {
				String parameter = s_modePrefixes[s_mode_byDay] + 
					start.getTime() + "," + end.getTime();
				
				if (excepts != null) {
					for (int n = 0; n < excepts.length; n++) {
						if (parameter.equals(excepts[n])) {
							continue outer;
						}
					}
				}
				
				buckets.add(
					new DateBucket(
						this.getClass().getName(), 
						concatWith + encodeParameter(parameter),
						MessageFormat.format(
								weekOfFormat,
								new Object[] {start}), 
						count,
						start));
			}
		}
		
		if (buckets.size() > 0) {
			bucketThemes.add(new BucketTheme(
				getResources(projection.getLocale()).getString("ByDay"),
				"distinct-values",
				buckets
			));
		}
	}
	
	protected void suggestNarrowingByHour(
			IDateTimeProjection projection, float desirable, List bucketThemes,
			String[] excepts, String concatWith) {
			
		String weekOfFormat = getResources(projection.getLocale()).getString("HourOfFormat");
		
		int totalCount = projection.countObjects();
		
		Calendar earliest = Calendar.getInstance();
		Calendar latest = Calendar.getInstance();
		
		earliest.setTime(projection.getEarliest());
		latest.setTime(projection.getLatest());
		
		Calendar earliestHour = Calendar.getInstance();
		earliestHour.clear(Calendar.MILLISECOND);
		earliestHour.clear(Calendar.SECOND);
		earliestHour.clear(Calendar.MINUTE);
		
		List buckets = new ArrayList();
		
		outer:while (earliestHour.before(latest)) {
			Date start = (Date) earliestHour.getTime().clone();
			earliestHour.add(Calendar.HOUR, 1);
			Date end = earliestHour.getTime();
			
			int count = projection.countObjects(start, end);
			if (count > 0 && count < totalCount) {
				String parameter = s_modePrefixes[s_mode_byHour] + 
					start.getTime() + "," + end.getTime();
				
				if (excepts != null) {
					for (int n = 0; n < excepts.length; n++) {
						if (parameter.equals(excepts[n])) {
							continue outer;
						}
					}
				}
				
				buckets.add(
					new DateBucket(
						this.getClass().getName(), 
						concatWith + encodeParameter(parameter),
						MessageFormat.format(
								weekOfFormat,
								new Object[] {start}), 
						count,
						start));
			}
		}
		
		if (buckets.size() > 0) {
			bucketThemes.add(new BucketTheme(
				getResources(projection.getLocale()).getString("ByHour"),
				"distinct-values",
				buckets
			));
		}
	}

	protected void suggestRelative(
		IDateTimeProjection projection, float desirable, List bucketThemes,
		String[] excepts, String concatWith) {
		
		Date start, end;
		
		Calendar c = null;
		List buckets = null;
		
		// Present
		buckets = new ArrayList();
		
		c = Calendar.getInstance(); clearThingsInDay(c);
		start = (Date) c.getTime().clone(); c.add(Calendar.DATE, 1); end = c.getTime();
		addRelativeBucket(projection, start, end, s_relativeMode_today, buckets, excepts, concatWith);
		
		c = Calendar.getInstance(); clearThingsInDay(c); c.clear(Calendar.DAY_OF_WEEK);
		start = (Date) c.getTime().clone(); c.add(Calendar.WEEK_OF_YEAR, 1); end = c.getTime();
		addRelativeBucket(projection, start, end, s_relativeMode_thisWeek, buckets, excepts, concatWith);
		
		c = Calendar.getInstance(); clearThingsInDay(c); c.clear(Calendar.DAY_OF_MONTH);
		start = (Date) c.getTime().clone(); c.add(Calendar.MONTH, 1); end = c.getTime();
		addRelativeBucket(projection, start, end, s_relativeMode_thisMonth, buckets, excepts, concatWith);
		
		c = Calendar.getInstance(); clearThingsInDay(c); c.clear(Calendar.DAY_OF_YEAR);
		start = (Date) c.getTime().clone(); c.add(Calendar.YEAR, 1); end = c.getTime();
		addRelativeBucket(projection, start, end, s_relativeMode_thisYear, buckets, excepts, concatWith);
		
		c = Calendar.getInstance(); 
		end = (Date) c.getTime().clone(); clearThingsInDay(c); c.add(Calendar.DATE, -7); start = c.getTime();
		addRelativeBucket(projection, start, end, s_relativeMode_withinAWeek, buckets, excepts, concatWith);
		
		c = Calendar.getInstance(); 
		end = (Date) c.getTime().clone(); clearThingsInDay(c); c.add(Calendar.MONTH, -1); start = c.getTime();
		addRelativeBucket(projection, start, end, s_relativeMode_withinAMonth, buckets, excepts, concatWith);
		
		c = Calendar.getInstance(); 
		end = (Date) c.getTime().clone(); clearThingsInDay(c); c.add(Calendar.MONTH, -3); start = c.getTime();
		addRelativeBucket(projection, start, end, s_relativeMode_within3Months, buckets, excepts, concatWith);

		c = Calendar.getInstance(); 
		end = (Date) c.getTime().clone(); clearThingsInDay(c); c.add(Calendar.MONTH, -6); start = c.getTime();
		addRelativeBucket(projection, start, end, s_relativeMode_withinHalfAYear, buckets, excepts, concatWith);

		c = Calendar.getInstance(); 
		end = (Date) c.getTime().clone(); clearThingsInDay(c); c.add(Calendar.YEAR, -1); start = c.getTime();
		addRelativeBucket(projection, start, end, s_relativeMode_withinAYear, buckets, excepts, concatWith);
		
		if (buckets.size() > 0) {
			bucketThemes.add(new BucketTheme(
				getResources(projection.getLocale()).getString("Present"),
				"distinct-values",
				buckets
			));
		}

		// Past
		buckets = new ArrayList();
		
		c = Calendar.getInstance(); clearThingsInDay(c);
		end = (Date) c.getTime().clone(); c.add(Calendar.DATE, -1); start = c.getTime();
		addRelativeBucket(projection, start, end, s_relativeMode_yesterday, buckets, excepts, concatWith);

		c = Calendar.getInstance(); clearThingsInDay(c); c.clear(Calendar.DAY_OF_WEEK);
		end = (Date) c.getTime().clone(); c.add(Calendar.WEEK_OF_YEAR, -1); start = c.getTime();
		addRelativeBucket(projection, start, end, s_relativeMode_lastWeek, buckets, excepts, concatWith);

		c = Calendar.getInstance(); clearThingsInDay(c); c.clear(Calendar.DAY_OF_MONTH);
		end = (Date) c.getTime().clone(); c.add(Calendar.MONTH, -1); start = c.getTime();
		addRelativeBucket(projection, start, end, s_relativeMode_lastMonth, buckets, excepts, concatWith);

		c = Calendar.getInstance(); clearThingsInDay(c); c.clear(Calendar.DAY_OF_YEAR);
		end = (Date) c.getTime().clone(); c.add(Calendar.YEAR, -1); start = c.getTime();
		addRelativeBucket(projection, start, end, s_relativeMode_lastYear, buckets, excepts, concatWith);
		
		c = Calendar.getInstance(); clearThingsInDay(c); c.add(Calendar.WEEK_OF_YEAR, -1);
		end = c.getTime(); start = null;
		addRelativeBucket(projection, start, end, s_relativeMode_before1Week, buckets, excepts, concatWith);
		
		c = Calendar.getInstance(); clearThingsInDay(c); c.add(Calendar.MONTH, -1);
		end = c.getTime(); start = null;
		addRelativeBucket(projection, start, end, s_relativeMode_before1Month, buckets, excepts, concatWith);
		
		c = Calendar.getInstance(); clearThingsInDay(c); c.add(Calendar.MONTH, -3);
		end = c.getTime(); start = null;
		addRelativeBucket(projection, start, end, s_relativeMode_before3Months, buckets, excepts, concatWith);
		
		c = Calendar.getInstance(); clearThingsInDay(c); c.add(Calendar.MONTH, -6);
		end = c.getTime(); start = null;
		addRelativeBucket(projection, start, end, s_relativeMode_beforeHalfAYear, buckets, excepts, concatWith);
		
		c = Calendar.getInstance(); clearThingsInDay(c); c.add(Calendar.YEAR, -1);
		end = c.getTime(); start = null;
		addRelativeBucket(projection, start, end, s_relativeMode_beforeAYear, buckets, excepts, concatWith);
		
		if (buckets.size() > 0) {
			bucketThemes.add(new BucketTheme(
				getResources(projection.getLocale()).getString("Past"),
				"distinct-values",
				buckets
			));
		}
		
		// Future
		buckets = new ArrayList();
		
		c = Calendar.getInstance(); clearThingsInDay(c); c.add(Calendar.DATE, 1);
		start = (Date) c.getTime().clone(); c.add(Calendar.DATE, 1); end = c.getTime();
		addRelativeBucket(projection, start, end, s_relativeMode_tomorrow, buckets, excepts, concatWith);
		
		c = Calendar.getInstance(); clearThingsInDay(c); c.clear(Calendar.DAY_OF_WEEK); c.add(Calendar.WEEK_OF_YEAR, 1);
		start = (Date) c.getTime().clone(); c.add(Calendar.WEEK_OF_YEAR, 1); end = c.getTime();
		addRelativeBucket(projection, start, end, s_relativeMode_nextWeek, buckets, excepts, concatWith);
		
		c = Calendar.getInstance(); clearThingsInDay(c); c.clear(Calendar.DAY_OF_MONTH); c.add(Calendar.MONTH, 1);
		start = (Date) c.getTime().clone(); c.add(Calendar.MONTH, 1); end = c.getTime();
		addRelativeBucket(projection, start, end, s_relativeMode_nextMonth, buckets, excepts, concatWith);
		
		c = Calendar.getInstance(); clearThingsInDay(c); c.clear(Calendar.DAY_OF_YEAR); c.add(Calendar.YEAR, 1);
		start = (Date) c.getTime().clone(); c.add(Calendar.YEAR, 1); end = c.getTime();
		addRelativeBucket(projection, start, end, s_relativeMode_nextYear, buckets, excepts, concatWith);
		
		if (buckets.size() > 0) {
			bucketThemes.add(new BucketTheme(
				getResources(projection.getLocale()).getString("Future"),
				"distinct-values",
				buckets
			));
		}
	}
	
	protected void addRelativeBucket(
		IDateTimeProjection projection, Date start, Date end, int relativeMode, List buckets,
		String[] excepts, String concatWith) {
		
		int totalCount = projection.countObjects();
		
		ResourceBundle resources = getResources(projection.getLocale());
		
		int count = projection.countObjects(start, end);
		if (count > 0 && count < totalCount) {
			String key = s_relativeModePrefixes[relativeMode];
			String parameter = s_modePrefixes[s_mode_relative] + key;
			
			if (excepts != null) {
				for (int n = 0; n < excepts.length; n++) {
					if (parameter.equals(excepts[n])) {
						return;
					}
				}
			}
			
			buckets.add(
				new DateBucket(
					this.getClass().getName(), 
					concatWith + encodeParameter(parameter),
					resources.getString(key), 
					count,
					start));
		}
	}
	
	/* (non-Javadoc)
	 * @see edu.mit.simile.longwell.query.bucket.BucketerBase#internalSuggestBroadeningBuckets(edu.mit.simile.longwell.query.project.IProjection, java.lang.String)
	 */
	protected BroadeningResult internalSuggestBroadeningBuckets(
			IProjection projection, String parameter) {
		
		ResourceBundle resources = getResources(projection.getLocale());
		List existingBuckets = new ArrayList();

		String[] params = splitParameter(parameter);
		for (int i = 0; i < params.length; i++) {
			String param = "";
			
			for (int j = 0; j < params.length; j++) {
				if (i != j) {
					if (param.length() == 0) {
						param += encodeParameter(params[j]);
					} else {
						param += "," + encodeParameter(params[j]);
					}
				}
			}
			
			int mode = parameterToMode(params[i]);
			if (mode < s_modePrefixes.length) {
				String label = modeToLabel(mode, params[i], resources);
			
				existingBuckets.add(
					new Bucket(
						this.getClass().getName(), 
						param,
						label,
						0));
			}
		}
		
		IDateTimeProjection dtProjection = (IDateTimeProjection) projection;

		List bucketThemes = 
			internalSuggestBuckets(dtProjection, 1, params, parameter + ",");
		
		suggestRelative(dtProjection, 1, bucketThemes, params, parameter + ",");
		
		return new BroadeningResult(existingBuckets, bucketThemes);
	}
	
	/* (non-Javadoc)
	 * @see edu.mit.simile.longwell.query.bucket.BucketerBase#individualParameterToDescription(java.lang.String, java.lang.String)
	 */
	protected String individualParameterToDescription(
			String parameter,
			String locale) {
		
		ResourceBundle resources = getResources(locale);
		int mode = parameterToMode(parameter);
		
		if (mode < s_modePrefixes.length) {
			return modeToLabel(mode, parameter, resources);
		} else {
			return "";
		}
	}
	
	protected int parameterToMode(String parameter) {
		int mode;
		for (mode = 0; mode < s_modePrefixes.length; mode++) {
			if (parameter.startsWith(s_modePrefixes[mode])) {
				break;
			}
		}
		return mode;
	}
	
	protected int parameterToRelativeMode(String parameter) {
		int relativeMode;
		for (relativeMode = 0; relativeMode < s_relativeModePrefixes.length; relativeMode++) {
			if (parameter.equals(s_relativeModePrefixes[relativeMode])) {
				break;
			}
		}
		return relativeMode;
	}
	
	protected String modeToLabel(int mode, String param, ResourceBundle resources) {
		String arguments = param.substring(s_modePrefixes[mode].length());
		String label = "";
		
		if (mode == s_mode_relative) {
			int relativeMode = parameterToRelativeMode(arguments);
			label = resources.getString(s_relativeModePrefixes[relativeMode]);
		} else {
			String[] dateStrings = arguments.split(",");
			
			Date start = new Date(Long.parseLong(dateStrings[0]));
			SimpleDateFormat simpleDateformat=new SimpleDateFormat("yyyy");
			Integer dateOK =  Integer.parseInt(simpleDateformat.format(start));
			Date end = new Date(Long.parseLong(dateStrings[1]));
			
			switch (mode) {
			case s_mode_byCentury:
				label = MessageFormat.format(
					resources.getString("ByCenturyFormat"),
					new Object[] { Integer.toString(start.getYear()) });
				break;
			case s_mode_byDecade:
				label = MessageFormat.format(
					resources.getString("ByDecadeFormat"),
					new Object[] { Integer.toString(dateOK) });
				break;
			case s_mode_byYear:
				//SimpleDateFormat simpleDateformat=new SimpleDateFormat("yyyy");
				label = MessageFormat.format(
					resources.getString("ByYearFormat"),
					new Object[] { Integer.toString(dateOK) });
				break;
			case s_mode_byMonth:
				label = MessageFormat.format(
					resources.getString("ByMonthFormat"),
					new Object[] { start });
				break;
			case s_mode_byWeek:
				label = MessageFormat.format(
					resources.getString("ByWeekFormat"),
					new Object[] { start });
				break;
			case s_mode_byDay:
				label = MessageFormat.format(
					resources.getString("ByDayFormat"),
					new Object[] { start });
				break;
			case s_mode_byHour:
				label = MessageFormat.format(
					resources.getString("ByHourFormat"),
					new Object[] { start });
				break;
			}
		}
		
		return label;
	}

	protected Date[] relativeModeToDates(int relativeMode) {
		Date start = null;
		Date end = null;
		
		Calendar now = Calendar.getInstance();
		now.clear(Calendar.HOUR_OF_DAY);
		now.clear(Calendar.MINUTE);
		now.clear(Calendar.SECOND);
		now.clear(Calendar.MILLISECOND);
		
		switch (relativeMode) {
		case s_relativeMode_today:
			start = (Date) now.getTime().clone();
			now.add(Calendar.DATE, 1);
			end = now.getTime();
			break;
		case s_relativeMode_thisWeek:
			now.clear(Calendar.DAY_OF_WEEK);
			start = (Date) now.getTime().clone();
			now.add(Calendar.WEEK_OF_YEAR, 1);
			end = now.getTime();
			break;
		case s_relativeMode_thisMonth:
			now.clear(Calendar.DAY_OF_MONTH);
			start = (Date) now.getTime().clone();
			now.add(Calendar.MONTH, 1);
			end = now.getTime();
			break;
		case s_relativeMode_thisYear:
			now.clear(Calendar.MONTH);
			start = (Date) now.getTime().clone();
			now.add(Calendar.YEAR, 1);
			end = now.getTime();
			break;
			
		case s_relativeMode_withinAWeek:
			end = (Date) now.getTime().clone();
			now.add(Calendar.DATE, -7);
			start = now.getTime();
			break;
		case s_relativeMode_withinAMonth:
			end = (Date) now.getTime().clone();
			now.add(Calendar.MONTH, -1);
			start = now.getTime();
			break;
		case s_relativeMode_within3Months:
			end = (Date) now.getTime().clone();
			now.add(Calendar.MONTH, -3);
			start = now.getTime();
			break;
		case s_relativeMode_withinHalfAYear:
			end = (Date) now.getTime().clone();
			now.add(Calendar.MONTH, -6);
			start = now.getTime();
			break;
		case s_relativeMode_withinAYear:
			end = (Date) now.getTime().clone();
			now.add(Calendar.YEAR, -1);
			start = now.getTime();
			break;
			
		case s_relativeMode_yesterday:
			end = (Date) now.getTime().clone();
			now.add(Calendar.DATE, -1);
			start = now.getTime();
			break;
		case s_relativeMode_lastWeek:
			now.clear(Calendar.DAY_OF_WEEK);
			end = (Date) now.getTime().clone();
			now.add(Calendar.WEEK_OF_YEAR, -1);
			start = now.getTime();
			break;
		case s_relativeMode_lastMonth:
			now.clear(Calendar.DAY_OF_MONTH);
			end = (Date) now.getTime().clone();
			now.add(Calendar.MONTH, -1);
			start = now.getTime();
			break;
		case s_relativeMode_lastYear:
			now.clear(Calendar.DAY_OF_YEAR);
			end = (Date) now.getTime().clone();
			now.add(Calendar.YEAR, -1);
			start = now.getTime();
			break;
			
		case s_relativeMode_before1Week:
			now.add(Calendar.WEEK_OF_YEAR, -1);
			end = now.getTime();
			break;
		case s_relativeMode_before1Month:
			now.add(Calendar.MONTH, -1);
			end = now.getTime();
			break;
		case s_relativeMode_before3Months:
			now.add(Calendar.MONTH, -3);
			end = now.getTime();
			break;
		case s_relativeMode_beforeHalfAYear:
			now.add(Calendar.MONTH, -6);
			end = now.getTime();
			break;
		case s_relativeMode_beforeAYear:
			now.add(Calendar.YEAR, -1);
			end = now.getTime();
			break;
			
		case s_relativeMode_tomorrow:
			now.add(Calendar.DATE, 1);
			start = now.getTime();
			break;
		case s_relativeMode_nextWeek:
			now.clear(Calendar.DAY_OF_WEEK);
			now.add(Calendar.WEEK_OF_YEAR, 1);
			start = now.getTime();
			break;
		case s_relativeMode_nextMonth:
			now.clear(Calendar.DAY_OF_MONTH);
			now.add(Calendar.MONTH, 1);
			start = now.getTime();
			break;
		case s_relativeMode_nextYear:
			now.clear(Calendar.DAY_OF_YEAR);
			now.add(Calendar.YEAR, 1);
			start = now.getTime();
			break;
		}
		
		return new Date[] { start, end };
	}
	
	protected boolean datesEqual(Date d1, Date d2) {
		if (d1 == null) {
			return d2 == null;
		} else {
			return d1.equals(d2);
		}
	}
	
	protected void clearThingsInDay(Calendar c) {
		c.clear(Calendar.HOUR_OF_DAY);
		c.clear(Calendar.MINUTE);
		c.clear(Calendar.SECOND);
		c.clear(Calendar.MILLISECOND);
	}
}

class DateBucket extends Bucket {
	final Date m_date;
	
	public DateBucket(
			String	bucketerName,
			String 	bucketerParameter,
			String	label,
			int 	count,
			Date	date) {
		super(bucketerName, bucketerParameter, label, count);
		m_date = date;
	}
}
class DateBucketComparator implements Comparator {
	/* (non-Javadoc)
	 * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
	 */
	public int compare(Object o1, Object o2) {
		DateBucket d1 = (DateBucket) o1;
		DateBucket d2 = (DateBucket) o2;

		return d1.m_date.compareTo(d2.m_date);
	}
	
}
